多态性可分为运行时的多态性(虚函数),和编译时的多态性(重载)
还有构造函数多态。
多态性是通过virtual函数和动态绑定实现的。
一、多态性有两种表现形式:
1)编译时多态性:同一对象在收到相同的消息却产生不同的函数调用,一般通过函数重载来实现,在编译时就实现了绑定,属于静态绑定。
2)运行时多态性:不同对象在收到相同消息时产生不同的动作,一般通过虚函数来实现。
二、多态和非多态的实质区别
函数地址为动态绑定还是静态绑定。virtual就是告诉编译器,函数不是使用静态绑定,而是要使用动态绑定。
三、多态的作用
对于封装,可以使代码模块化。
对于继承,可以扩展已经存在的代码。
而多态,是为了接口重用。同样的功能不要再增加杂七杂八的名字。
四、多态最常见的用法
最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。因为没有多态性,函数调用的地址将是一定的,而固定的地址将始终调用到同一个函数,这就无法实现一个接口,多种方法的目的了。
五、扩展了解隐藏规则
本来仅仅区别重载与覆盖并不算困难,但是C++的隐藏规则使问题复杂性陡然增加。
这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏。(注意别和重载混淆)
2)如果派生类的函数与基类的函数相同,但是基类没有virtual关键字,那么基类的函数将被隐藏。(注意别和覆盖混淆)。
多态性允许不同类的对象对同一消息做出不同的响应。静态多态性:函数重载;动态多态性:通过虚函数来实现
动态多态性发生在程序运行期,是动态绑定。动态多态则是通过继承、虚函数、指针来实现。程序在运行时才决定调用的函数,通过父类指针调用子类的函数,可以让父类指针具有多种形态。
1 在基类中的某成员函数被声明为虚函数后,在之后的派生类中可以重新来定义它。但定义时,其函数原型、函数名、参数个数、参数类型的顺序,都必须和基类中的原型完全相同
2 必须通过基类指针指向派生类,才能通过虚函数实现运行时的多态性;
3 虚函数具有继承性,只要在基类中显式声明了虚函数,在派生类中函数名前的virtual可以略去,因为系统会根据其是否和基类中虚函数原型完全相同来判断是不是虚函数。一个虚函数无论被公有继承了多少次,它仍然是虚函数;
4 使用虚函数,派生类必须是基类公有派生的。
5 虚函数必须是所在类的成员函数,而不能是友元函数,也不能是静态成员函数。因为虚函数调用要靠特定的对象类决定应该激活哪一个函数。
6 构造函数不能是虚函数,但析构函数可以是虚函数;
虚函数只是定义一类相同的行为,通过重载成具体的个体的函数实现个体不同的行为方式;
名字相同,代表共同的行为,但重载后的派生类有了差异性,但由于虚函数的指针有了不同的指向(不同的派生类对象),因而可以产生不同的行为。
虚函数的主要作用是将其类和各个派生类的通用功能抽象出来,实现通用部分,而在派生类中写特定代码。因此,从多个对象抽象出功能交集是非常重要的,而交集中的功能使用虚函数实现。有时基类会为这些虚函数提供一个默认的实现函数。
纯虚函数是指不指定任何实现的函数。如果类只包含纯虚函数,或从纯虚函数继承而来,并且没有提供任何实现,则该类为抽象类。程序中不能创建抽象类对象,只能使用抽象类进行派生,而所有从抽象类派生而来的类必须实现纯虚函数。线虚函数使用纯指示符(=0)声明,代码如下:
virtual CalArea() = 0;
抽象类提供通用功能,但是因为太通用而无法实现具体的功能。
为了方便使用多态特性,编程者常常需要在基类中定义虚拟函数。在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出猴子、犀牛等子类, 但动物本身生成对象明显不合常理。
为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数,则编译器要求在派生类中必须予以重载以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。
多态性使得能够利用同一类(基类)类型的指针来引用不同类的对象,以及根据所引用对象的不同,以不同的方式执行相同的操作。
基础好的C/C++程序员应该明白,实现多态主要采用引用和指针。传值这种方式是复制数据,其类型编译器就已决定,而多态是类型要等到执行期才能决定,所以不使用传值方式来实现多态参数传递。
引入抽象基类和纯虚函数的主要目的是为了实现一种接口的效果。在面向对象的编程语言中,为了更好的表示客观世界。所以有些类可以什么都不实现只是提供一个共享的接口。这就是纯虚函数,而含有纯虚函数的基类即为抽象基类。
多态的作用
多态是面向对象开发的一个特色,其作用主要是以下两个。
隐藏实现细节,使得代码能够模块化;扩展代码模块,实现代码重用。
接口重用:为了类在继承和派生的时候,保证使用家族中任一类的实例的某一属性时的正确调用。
多态性是如何实现程序的可扩展性?
派生类是一类特殊的基类,例如本科生、硕士生和博士生都是从学生类派生的。由于所有学生都要写论文,所以基类(学生类)中有一个“写论文”的虚函数。但每类学生写论文的要求是不一样的,因此在本科生、硕士生和博士生类中都有一个对应于虚函数的“写论文”函数。当需要向所有学生布置写论文的任务时,可以用一个学生类的指针遍历所有的学生。由于多态性,每类学生执行的是自己类新增的“写论文”函数,按照自己类规定的要求写论文。如果学校决定要招收工程硕士,那么系统中必须有一个“工程硕士”的类。工程硕士也是一类学生,所以也从学生类派生。工程硕士也要写论文,论文要求和其他几类学生不同,所以工程硕士类也要有一个“写论文”的函数。如果建好了工程硕士这个类,向全校学生布置写论文的工作流程还和以前一样。由此可见,当扩展系统时,只需要增加一些新的类,而主程序不变。
多态的关键在于编译器行为:建立虚函数表、为对象建立虚函数指针;
1、在定义纯虚函数时,不能定义虚函数的实现部分。
2、把函数名赋于0,本质上是将指向函数体的指针值赋为初值0。与定义空函数不一样,空函数的函数体为空,即调用该函数时,不执行任何动作。在没有重新定义这种纯虚函数之前,是不能调用这种函数的。
3、把至少包含一个纯虚函数的类,称为抽象类。这种类只能作为派生类的基类,不能用来说明这种类的对象。
其理由是明显的:因为虚函数没有实现部分,所以不能产生对象。但可以定义指向抽象类的指针,即指向这种基类的指针。当用这种基类指针指向其派生类的对象时,必须在派生类中重载纯虚函数,否则会产生程序的运行错误。
4、在以抽象类作为基类的派生类中必须有纯虚函数的实现部分,即必须有重载纯虚函数的函数体。否则,这样的派生类也是不能产生对象的。
综上所述,可把纯虚函数归结为:抽象类的唯一用途是为派生类提供基类,纯虚函数的作用是作为派生类中的成员函数的基础,并实现动态多态性。
动态多态:纵向多态;函数重载和模板:横向多态;
Polymorphism is A single name can have multiple meanings depending on its context.
所谓多态,简单地讲,就是指一个名字(或符号)具有多种含义。
首先考虑成员函数所在的类是否会做为基类。然后看成员函数在类的继承后有无功能被修改?如果希望修改其功能,一般将它声明为虚函数。
如果成员函数在类被继承之后功能不需要修改,或派生类中用不到该函数,则不要把它声明为虚函数。
应当考虑对成员函数的调用是通过对象名还是基类指针或引用去访问。如果通过基类指针或引用去访问,则声明为虚函数。
如果希望通过基类指针或者引用访问派生类成员函数,但基类功能比较抽象或者不能确定功能,可以将基类定义为抽象类,即只定义函数名字,没有函数体,具体功能由派生类添加。
对象销毁时(离开对象的作用域后),需要调用析构函数。在多态调用时,是用基类的指针访问派生类的对象。如果析构函数是非虚函数,则基类指针只能访问基类的析构函数,而不能访问派生类的析构函数,导致派生类对象销毁时,没有调用派生类的析构函数,只是调用了基类的析构函数。如果把析构函数定义成虚函数,则可克服这个问题。其实就是通过virtual这个定义在析构函数前的关键字,告诉编译器,需要调用子类的析构函数做析构。
多态类中的虚函数表是Compile-Time,还是Run-Time时建立的?
虚拟函数表是在编译期就建立了,各个虚拟函数这时被组织成了一个虚拟函数的入口地址的数组。而对象的隐藏成员--虚拟函数表指针是在运行期--也就是构造函数被调用时进行初始化的,这是实现多态的关键。
class B:virtual public A{}虚函数的使用方法是在基类用virtual声明成员函数为虚函数。